//package com.kaiyun.score.everton.server.service.impl;
//
//import com.kaiyun.score.barcelona.entity.R;
//import com.kaiyun.score.barcelona.utils.StringUtils;
//import com.kaiyun.score.everton.api.constant.NewsSourceEnum;
//import com.kaiyun.score.everton.api.entity.News;
//import com.kaiyun.score.everton.server.repository.NewsRepository;
//import com.kaiyun.score.everton.server.service.CrawlerService;
//import com.kaiyun.score.everton.server.service.impl.parser.*;
//import com.kaiyun.score.everton.server.utils.HttpClientUtil;
//import com.kaiyun.score.object.storage.service.FileUploadService;
//import com.kaiyun.score.object.storage.utils.ImageProcessUtil;
//import io.netty.handler.timeout.TimeoutException;
//import jakarta.annotation.PostConstruct;
//import jakarta.annotation.Resource;
//import lombok.RequiredArgsConstructor;
//import org.jsoup.Jsoup;
//import org.jsoup.nodes.Document;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.stereotype.Service;
//import org.springframework.util.DigestUtils;
//
//import java.io.InputStream;
//import java.nio.file.Files;
//import java.nio.file.Path;
//import java.nio.file.StandardOpenOption;
//import java.util.*;
//import java.util.concurrent.CompletableFuture;
//import java.util.concurrent.ExecutorService;
//import java.util.concurrent.Executors;
//import java.util.concurrent.TimeUnit;
//
//@Service
//@RequiredArgsConstructor
//public class CrawlerServiceImpl implements CrawlerService {
//
//    private final static Logger LOGGER = LoggerFactory.getLogger(CrawlerServiceImpl.class);
//
//    @Resource
//    private FileUploadService fileUploadService;
//    @Resource
//    private NewsRepository newsRepository;
//    @Resource
//    private HttpClientUtil httpClientUtil;
//
//    private final Map<String, AbstractNewsParser> parserMap = new HashMap<>();
//    private final ExecutorService executorService = Executors.newFixedThreadPool(10);
//
//    @PostConstruct
//    private void initParsers() {
//        LOGGER.info("【爬虫系统】开始初始化解析器，httpClientUtil是否为null：{}", httpClientUtil == null);
//
//        parserMap.put(NewsSourceEnum.FLASHSCORE.getName(), new FlashScoreParser());
//        parserMap.put(NewsSourceEnum.PREMIER_LEAGUE.getName(), new PremierLeagueParser(httpClientUtil)); // 动态页面，所以抓api接口
//        parserMap.put(NewsSourceEnum.LA_LIGA.getName(), new LaLigaParser());
//        parserMap.put(NewsSourceEnum.BUNDESLIGA.getName(), new BundesligaParser());
//        parserMap.put(NewsSourceEnum.SERIE_A.getName(), new SerieAParser());
//        parserMap.put(NewsSourceEnum.LIGUE_1.getName(), new Ligue1Parser());
//
//        LOGGER.info("【爬虫系统】解析器初始化完成，parserMap大小：{}", parserMap.size());
//    }
//
//    @Override
//    public void crawlNews(NewsSourceEnum source) {
//        LOGGER.info("【爬虫系统】开始爬取{}的新闻", source.getName());
//
//        try {
//            // 1. 获取列表页
//            String html = httpClientUtil.get(source.getBaseUrl());
//
//            LOGGER.info("【爬虫系统】HTML内容包含'news'的次数：{}",
//                    html.split("news", -1).length - 1);
//            LOGGER.info("【爬虫系统】HTML内容包含'media-thumbnail'的次数：{}",
//                    html.split("media-thumbnail", -1).length - 1);
//
////            // 打印HTML的前2000个字符
////            LOGGER.info("【爬虫系统】HTML前6000字符：{}",
////                    html.substring(0, Math.min(6000, html.length())));
//
//            Document doc = Jsoup.parse(html);
//
//            // 2. 解析文章链接
//            AbstractNewsParser parser = parserMap.get(source.getName());
//            List<String> articleLinks = parser.parseArticleLinks(doc);
//            LOGGER.info("【爬虫系统】从{}获取到{}个文章链接", source.getName(), articleLinks.size());
//
//            // 3. 并发爬取文章详情
//            List<CompletableFuture<News>> futures = articleLinks.stream()
//                    .map(link -> CompletableFuture.supplyAsync(() ->
//                            crawlSingleArticle(link, source.getName()), executorService))
//                    .toList();
//
//            // 4. 等待所有任务完成
//            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
//
//            // 5. 收集结果
//            for (CompletableFuture<News> future : futures) {
//                News news = future.get();
//                if (news != null) {
//                    // 打印
//                    LOGGER.info("【爬虫系统】爬取文章成功：{}", news);
//                }
//            }
//
//        } catch (Exception e) {
//            LOGGER.error("【爬虫系统】爬取{}失败", source.getName(), e);
//        }
//
//    }
//
//    @Override
//    public News crawlSingleArticle(String url, String sourceName) {
//        try {
//            // 生成MD5用于去重
//            String md5 = DigestUtils.md5DigestAsHex(url.getBytes());
//            if (newsRepository.existsByMd5Hash(md5)) {
//                LOGGER.debug("【爬虫系统】文章已存在，跳过：{}", url);
//                return null;
//            }
//
//            // 获取文章页面
//            String html = httpClientUtil.get(url);
//            Document doc = Jsoup.parse(html);
//
//            // 解析文章
//            AbstractNewsParser parser = parserMap.get(sourceName);
//            News article = parser.parseArticle(doc, url);
//            article.setMd5Hash(md5);
//
//            // 打印整条
//            LOGGER.info("【爬虫系统】爬取文章成功：{}", article);
//
//            if (StringUtils.isBlank(article.getContent())) {
//                LOGGER.debug("【爬虫系统】文章内容为空，跳过：{}", url);
//                return null;
//            }
//
//            if (StringUtils.isNotBlank(article.getCoverUrl())) {
//                // 封面不为空，则设为原始封面
//                article.setOriginalCoverUrl(article.getCoverUrl());
//
//                // 下载图片设为新封面
//                String localPath = downloadSingleImage(article.getCoverUrl(), "/news/" + md5);
//                article.setCoverUrl(localPath);
//            }
//
//            LOGGER.info("【爬虫系统】下载图片成功：{}", article.getCoverUrl());
//
//            // 保存到数据库
//            return newsRepository.save(article);
//        } catch (Exception e) {
//            LOGGER.error("【爬虫系统】爬取文章失败：{}", url, e);
//            return null;
//        }
//    }
//
////    public String downloadSingleImage(String imageUrl, String targetName) {
////        if (StringUtils.isBlank(imageUrl)) {
////            LOGGER.warn("【爬虫系统】图片URL为空，跳过下载");
////            return null;
////        }
////
////        try {
////            LOGGER.info("【爬虫系统】开始下载图片：{}", imageUrl);
////            long startTime = System.currentTimeMillis();
////
////            R<String> result = fileUploadService.uploadImageFromUrl(imageUrl, targetName);
////
////            long endTime = System.currentTimeMillis();
////            LOGGER.info("【爬虫系统】图片下载耗时：{}ms", endTime - startTime);
////
////            if (result != null && result.getData() != null) {
////                String localPath = result.getData();
////                LOGGER.info("【爬虫系统】图片下载成功：{} -> {}", imageUrl, localPath);
////                return localPath;
////            } else {
////                LOGGER.warn("【爬虫系统】图片下载失败：{}，原因：{}",
////                        imageUrl, result != null ? result.getMsg() : "结果为空");
////                return null;
////            }
////
////        } catch (Exception e) {
////            LOGGER.error("【爬虫系统】下载图片异常：{}", imageUrl, e);
////            return null;
////        }
////    }
//
//    // 下载后压缩再上传
//    public String downloadSingleImage(String imageUrl, String targetName) {
//        if (StringUtils.isBlank(imageUrl)) {
//            LOGGER.warn("【爬虫系统】图片URL为空，跳过下载");
//            return null;
//        }
//
//        Path tempFile = null;
//        try {
//            long start = System.currentTimeMillis();
//
//            // 1. 下载+压缩（ImageProcessUtil 保证一定压缩过）
//            tempFile = ImageProcessUtil.downloadAndCompress(imageUrl);
//
//            // 2. 上传
//            try (InputStream in = Files.newInputStream(tempFile, StandardOpenOption.READ)) {
//
//                long costDownload = System.currentTimeMillis() - start;
//                LOGGER.debug("【爬虫系统】下载+压缩耗时：{}ms", costDownload);
//
//                R<String> result = fileUploadService.upload(in, targetName, ".jpg");
//
//                if (result != null && result.getData() != null) {
//                    LOGGER.info("【爬虫系统】图片上传成功：{} -> {}", imageUrl, result.getData());
//                    return result.getData();
//                } else {
//                    LOGGER.warn("【爬虫系统】上传失败：{}，原因：{}",
//                            imageUrl, result != null ? result.getMsg() : "未知");
//                    return null;
//                }
//            }
//
//        } catch (Exception e) {
//            LOGGER.error("【爬虫系统】处理图片异常：{}", imageUrl, e);
//            return null;
//
//        } finally {
//            // 3. 总会执行：删除临时文件
//            ImageProcessUtil.deleteQuietly(tempFile);
//        }
//    }
//
//    @Override
//    public void crawlAllSources() {
//        // 全部爬取
//        Arrays.stream(NewsSourceEnum.values())
//                .parallel()
//                .forEach(this::crawlNews);
//    }
//}

// 上面为旧版本，先全量保留

package com.wsh.crawler.service.impl;


import com.wsh.crawler.constant.NewsSourceEnum;
import com.wsh.crawler.entity.News;
import com.wsh.crawler.repository.NewsRepository;
import com.wsh.crawler.service.CrawlerService;
import com.wsh.crawler.service.impl.parser.*;
import com.wsh.crawler.utils.HttpClientUtil;
import com.wsh.crawler.utils.ImageProcessUtil;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.stream.Collectors;

@Service
public class CrawlerServiceImpl implements CrawlerService {

    private static final Logger LOGGER = LoggerFactory.getLogger(CrawlerServiceImpl.class);

    /*@Resource
    private FileUploadService fileUploadService;*/
    @Resource
    private NewsRepository newsRepository;
    @Resource
    private HttpClientUtil httpClientUtil;

    @Resource(name = "articleExecutor")
    private ThreadPoolTaskExecutor articleExecutor;

    private final Map<String, AbstractNewsParser> parserMap = new HashMap<>();

    private static final Duration ARTICLE_BATCH_TIMEOUT = Duration.ofMinutes(3); // 单来源文章整体超时
    // 如需更严的单篇超时，可在 httpClientUtil 中设置 connect/read/call 超时

    @PostConstruct
    private void initParsers() {
        LOGGER.info("【爬虫系统】开始初始化解析器，httpClientUtil是否为null：{}", httpClientUtil == null);

        parserMap.put(NewsSourceEnum.FLASHSCORE.getName(), new FlashScoreParser());
        parserMap.put(NewsSourceEnum.PREMIER_LEAGUE.getName(), new PremierLeagueParser(httpClientUtil)); // 动态页面，走 API
        parserMap.put(NewsSourceEnum.LA_LIGA.getName(), new LaLigaParser());
        parserMap.put(NewsSourceEnum.BUNDESLIGA.getName(), new BundesligaParser());
        parserMap.put(NewsSourceEnum.SERIE_A.getName(), new SerieAParser());
        parserMap.put(NewsSourceEnum.LIGUE_1.getName(), new Ligue1Parser());

        LOGGER.info("【爬虫系统】解析器初始化完成，parserMap大小：{}", parserMap.size());
    }

    @Override
    public void crawlNews(NewsSourceEnum source) {
        LOGGER.info("【爬虫系统】开始爬取{}的新闻", source.getName());
        try {
            // 1) 列表页
            String html = httpClientUtil.get(source.getBaseUrl());

            LOGGER.info("【爬虫系统】HTML包含标识计数 news={} media-thumbnail={}",
                    html.split("news", -1).length - 1,
                    html.split("media-thumbnail", -1).length - 1);

            Document doc = Jsoup.parse(html);

            // 2) 解析文章链接
            AbstractNewsParser parser = parserMap.get(source.getName());
            List<String> articleLinks = parser.parseArticleLinks(doc);
            // 去重（有些站点列表可能重复）
            articleLinks = articleLinks.stream().distinct().collect(Collectors.toList());

            LOGGER.info("【爬虫系统】从{}获取到{}个文章链接（去重后）", source.getName(), articleLinks.size());

            if (articleLinks.isEmpty()) {
                return;
            }

            // 3) 并发抓取文章详情（受管线程池 + 整体超时）
            List<Callable<News>> tasks = new ArrayList<>(articleLinks.size());
            for (String link : articleLinks) {
                tasks.add(() -> crawlSingleArticle(link, source.getName()));
            }

            // 整体超时：超过即自动 cancel 未完成任务
            List<Future<News>> futures = articleExecutor.getThreadPoolExecutor()
                    .invokeAll(tasks, ARTICLE_BATCH_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);

            // 4) 收集结果
            int success = 0, failed = 0, canceled = 0;
            for (Future<News> f : futures) {
                try {
                    if (f.isCancelled()) {
                        canceled++;
                        continue;
                    }
                    News news = f.get(); // 此时不会阻塞太久
                    if (news != null) {
                        success++;
                        LOGGER.info("【爬虫系统】爬取文章成功：{}", news);
                    } else {
                        failed++;
                    }
                } catch (CancellationException ce) {
                    canceled++;
                } catch (ExecutionException ee) {
                    failed++;
                    LOGGER.error("【爬虫系统】文章任务执行异常", ee.getCause());
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    failed++;
                    LOGGER.error("【爬虫系统】文章任务被中断", ie);
                }
            }

            LOGGER.info("【爬虫系统】{} 文章任务统计：成功={} 失败={} 取消={}（整体超时={}）",
                    source.getName(), success, failed, canceled, ARTICLE_BATCH_TIMEOUT);

        } catch (Exception e) {
            LOGGER.error("【爬虫系统】爬取{}失败", source.getName(), e);
        }
    }

    @Override
    public News crawlSingleArticle(String url, String sourceName) {
        try {
            // 去重：URL MD5
            String md5 = DigestUtils.md5DigestAsHex(url.getBytes());
            if (newsRepository.existsByMd5Hash(md5)) {
                LOGGER.debug("【爬虫系统】文章已存在，跳过：{}", url);
                return null;
            }

            // 获取文章页面
            String html = httpClientUtil.get(url);
            Document doc = Jsoup.parse(html);

            // 解析文章
            AbstractNewsParser parser = parserMap.get(sourceName);
            News article = parser.parseArticle(doc, url);
            article.setMd5Hash(md5);

            if (StringUtils.isBlank(article.getContent())) {
                LOGGER.debug("【爬虫系统】文章内容为空，跳过：{}", url);
                return null;
            }

            // 处理封面
            if (StringUtils.isNotBlank(article.getCoverUrl())) {
                article.setOriginalCoverUrl(article.getCoverUrl());
                //先不处理封面
                //String localPath = downloadSingleImage(article.getCoverUrl(), "/news/" + md5);
                //article.setCoverUrl(localPath);
                LOGGER.info("【爬虫系统】下载图片成功：{}", article.getCoverUrl());
            }

            // 保存
            return newsRepository.save(article);

        } catch (Exception e) {
            LOGGER.error("【爬虫系统】爬取文章失败：{}", url, e);
            return null;
        }
    }

    // 下载后压缩再上传
    public String downloadSingleImage(String imageUrl, String targetName) {
        if (StringUtils.isBlank(imageUrl)) {
            LOGGER.warn("【爬虫系统】图片URL为空，跳过下载");
            return null;
        }

        Path tempFile = null;
        try {
            long start = System.currentTimeMillis();

            // 1) 下载+压缩
            tempFile = ImageProcessUtil.downloadAndCompress(imageUrl);

            // 2) 上传
            try (InputStream in = Files.newInputStream(tempFile, StandardOpenOption.READ)) {
                long costDownload = System.currentTimeMillis() - start;
                LOGGER.debug("【爬虫系统】下载+压缩耗时：{}ms", costDownload);

                /*R<String> result = fileUploadService.upload(in, targetName, ".jpg");
                if (result != null && result.getData() != null) {
                    LOGGER.info("【爬虫系统】图片上传成功：{} -> {}", imageUrl, result.getData());
                    return result.getData();
                } else {
                    LOGGER.warn("【爬虫系统】上传失败：{}，原因：{}",
                            imageUrl, result != null ? result.getMsg() : "未知");
                    return null;
                }*/
                return null;
            }

        } catch (Exception e) {
            LOGGER.error("【爬虫系统】处理图片异常：{}", imageUrl, e);
            return null;

        } finally {
            ImageProcessUtil.deleteQuietly(tempFile);
        }
    }

    @Override
    public void crawlAllSources() {
        // 避免 parallel() 进入公共 ForkJoinPool，使用顺序执行更可控
        for (NewsSourceEnum src : NewsSourceEnum.values()) {
            crawlNews(src);
        }
    }
}